package com.googlecode.project4uz.encoder;

import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.openrdf.rio.RDFFormat;

import com.googlecode.project4uz.encoder.RioWriter.Reference;
import com.googlecode.project4uz.interfaces.Loggable;

/**
 * Klasa abstrakcyjna implementujaca wspolna funkcjonalnosc dla klas odczytujacych pola obiektow.
 * @author Daniel Kur
 */
public abstract class AbstractSerializer implements Loggable {
	/**
	 * Sposob serializacji obiektow.
	 */
	static AbstractSerializer serializer;
	
	/**
	 * Reprezentuje wlasciciela, ktory wskazuje na nie serializowany obiekt.
	 */
	protected Object owner;
	
	/**
	 * Reprezentacja referencji do serializowanych obiektow.
	 */
	protected Map<Integer, String> reference;

	/**
	 * Zapisujacy serializowane klasy do formatu RDF.
	 */
	protected RioWriter writer;
	
	/**
	 * Tworzy obiekt odczytujacy publiczne pola z innych obiektow.
	 * @param output - strumien wyjsciowy.
	 * @param format - format zapisu RDF.
	 */
	AbstractSerializer(final RDFFormat format, final OutputStream output) {
		this.reference = new HashMap<Integer, String>();
		this.writer = new RioWriter(format, output);
	}
	
	/**
	 * Tworzy i zwraca obiekt serializera.
	 * @param serializerType - typ uzytego serializatora.
	 * @return serializer - obiekt serializatora.
	 */
	public static AbstractSerializer createSerializer(final AbstractSerializer serializerType) {
		return AbstractSerializer.serializer = serializerType;
	}
	
	/**
	 * Odczytuje publiczne pola z dowolnego obiektu.
	 * @param object - dowolny obiekt do refleksji.
	 */
	abstract void readObject(final Object object);
	
	/**
	 * Odczytuje publiczne pola z dowolnego obiektu referencyjnego.
	 * @param object - dowolny obiekt do refleksji,
	 * @param ref - obiekt zapisujacy referencje w formacie RDF.
	 */
	abstract void readReference(final Object object, final Reference ref);
	
	/**
	 * Pobiera wartosc okreslonego pola z klasy.
	 * @param fromClass - z jakiej klasy,
	 * @param fromField - ktore pole,
	 * @return obiekt wskazany przez parametry.
	 */
	protected final Object readPrimitive(final Object fromClass, final Field fromField) {
		try {
			return fromField.get(fromClass);
		} catch(Exception e) {
			LOG.warning("Nie moge odczytac pola: " + fromField + " z klasy: " + fromClass);
			return null;
		}
	}
	
	/**
	 * Odczytuje pola zarowno jednowymiarowych, jak i wielowymiarowych tablic.
	 * @param array - tablica danych,
	 * @param tab - obiekt zapisujacy tablice w formacie RDF.
	 */
	protected final void readArray(final Object array, final Reference.Array tab) {
		if(isSimpleType(array.getClass().getComponentType())) {
			for(int i = 0; i < Array.getLength(array); i++) {
				if(Array.get(array, i) == null) {
					tab.writeNull();
				} else {
					tab.writePrimitive(Array.get(array, i).toString());
				}
			}
		} else if(array.getClass().getComponentType().isArray()) {
			for(int i = 0; i < Array.getLength(array); i++) {
				if(Array.get(array, i) == null) {
					tab.writeNull();
				} else {
					RioWriter.Reference.Array dim = tab.writeArray(Array.getLength(Array.get(array, i)));
					this.readArray(Array.get(array, i), dim);
				}
			}
		} else {
			for(int i = 0; i < Array.getLength(array); i++) {
				if(Array.get(array, i) == null) {
					tab.writeNull();
				} else {
					RioWriter.Reference ref = tab.writeReference();
					this.readReference(Array.get(array, i), ref);
				}
			}
		}
	}
	
	/**
	 * Odczytuje kolejne pola w kolekcjach.
	 * @param collection - kolekcja danych,
	 * @param col - obiekt zapisujacy kolekcje w formacie RDF.
	 */
	protected final void readCollection(final Collection<?> collection, final Reference.Collection col) {
		Iterator<?> i = collection.iterator();
		while (i.hasNext()) {
			Object value = i.next();
			if (isSimpleType(value.getClass())) {
				col.writePrimitive(value.toString()); 
			} else {
				RioWriter.Reference ref = col.writeReference();
				this.readReference(value, ref);
			}
		}
	}
	
	/**
	 * Odczytuje kolejne pola w mapach.
	 * @param map - mapa danych,
	 * @param m - obiekt zapisujacy mapy w formacie RDF.
	 */
	protected final void readMap(final Map<?, ?> map, final Reference.Map m) {
		Iterator<?> i = map.entrySet().iterator();
		while(i.hasNext()) {
			Map.Entry<?, ?> value =(Map.Entry<?, ?>)i.next();
			if(isSimpleType(value.getKey().getClass())) {
				m.writePrimitiveKey(value.getKey().toString());
			} else {
				RioWriter.Reference refKey = m.writeKey();
				this.readReference(value.getKey(), refKey);
			}
			
			if(isSimpleType(value.getValue().getClass())) {
				m.writePrimitiveValue(value.getValue().toString());
			} else {
				RioWriter.Reference refVal = m.writeValue();
				this.readReference(value.getValue(), refVal);
			}
		}
	}
	
	/**
	 * Odczytuje prywatne pole serialVersionUID klasy.
	 * @param object - do odczytu serialVersionUID.
	 * @param cls - obiekt zapisujacy referencje w formacie RDF.
	 */
	protected final void readSerialVersion(final Object object, final Reference cls) {
		try {
			Field serialField = object.getClass().getDeclaredField("serialVersionUID");
			serialField.setAccessible(true);
			Long serialVersionUID =(Long)readPrimitive(object, serialField);
			cls.writeSerial(serialVersionUID.toString());
		} catch(SecurityException e) {
			LOG.warning("Nie moge odczytac pola: serialVersionUID z klasy: " + object.getClass().getName());
		} catch(NoSuchFieldException e) {
			LOG.warning("Nie moge serializowac klasy: "+ object.getClass().getName() + " - brak pola serialVersionUID");
		}
	}
	
	/**
	 * Sprawdza podany typ, czy jest referencyjny, opakowywujacy czy primitywny (prosty).
	 * @param type - dowolna klasa stworzona na podstawie obiektu,
	 * @return true typ jest prosty, false typ jest referencyjny
	 */
	protected final boolean isSimpleType(final java.lang.Class<?> type) {
		return(type.isPrimitive()  || type.equals(String.class) || type.equals(Boolean.class) || type.equals(Byte.class) ||
				type.equals(Character.class) || type.equals(Short.class) || type.equals(Integer.class) ||
				type.equals(Long.class) || type.equals(Float.class) || type.equals(Double.class));
	}
	
	/**
	 * Pobiera unikalna wartosc reprezentujaca dana referencje.
	 * @param hashCode - obiektu referencyjnego do porownania.
	 * @return id tekstowy identyfikator referencji.
	 */
	protected final String getIdentificator(final int hashCode) {
		Iterator<Map.Entry<Integer, String>> i = reference.entrySet().iterator();
		while(i.hasNext()) {
			Map.Entry<Integer, String> value = i.next();
			if(value.getKey().equals(hashCode))
				return value.getValue();
		}
		return null;
	}
	
	/**
	 * Pobiera wartosc bedaca iloscia wymiarow w tablicy.
	 * @param array - tablica danych dla ktorej okreslamy wymiar.
	 * @return wymiar ktory posiada tablica.
	 */
	protected final int getDimension(final Object array) {
		int dim = 0;
		java.lang.Class<?> cls = array.getClass();
		while(cls.isArray()) {
			dim++;
			cls = cls.getComponentType();
		}
		return dim;
	}
	
	/**
	 * Pobiera typ wlasciwy dowolnej tablicy.
	 * @param array - tablica danych do przeskanowania.
	 * @return typ ktory posiada tablica.
	 */
	protected final String getComponentType(final Object array) {
		String type = null;
	    java.lang.Class<?> cls = array.getClass();
	    while(cls.isArray()) {
	    	type = cls.getComponentType().getName();
	    	cls = cls.getComponentType();
	    }
	    return type;
	}
	
	/**
	 * Dopisuje komentarz do strumienia.
	 * @param comment - tresc komentarza.
	 */
	protected final void writeComment(final String comment) {
		this.writer.writeComment(comment);
	}
	
	/**
	 * Dopisuje deklaracje do strumienia.
	 * @param stmt - beansowa deklaracja.
	 */
	abstract void readStatement(final java.beans.Statement stmt);
	
	/**
	 * Dopisuje wyrazenie do strumienia.
	 * @param exp - beansowe wyrazenie.
	 */
	abstract void readExpression(final java.beans.Expression exp);
	
	/**
	 * Pobiera typ wlasciciela, ktory nie jest serializowany.
	 * @return owner - typ wlasciciela.
	 */
	public Object getOwner() {
		return owner;
	}

	/**
	 * Ustawia typ wlasciciela, ktory nie jest serializowany.
	 * @param owner - typ wlasciciela.
	 */
	public void setOwner(final Object owner) {
		this.owner = owner;
		Reference own = writer.new Reference(owner.getClass().getName());
		writer.addOwner("serialization", owner.getClass().getName(), own.getID());
		reference.put(owner.hashCode(), own.getID());
	}
	
	/**
	 * Oproznia strumien z serializowanymi obiektami.
	 */
	protected final void flush() {
		this.writer.flush();
	}
	
	/**
	 * Zamyka strumien z serializowanymi obiektami.
	 */
	protected final void close() {
		this.writer.close();
	}
}
